Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import {AtpAgent} from '@atproto/api'
2
3import {type AnyProfileView} from '#/types/bsky/profile'
4
5type PResp = Awaited<ReturnType<AtpAgent['getProfile']>>
6
7// based on https://github.com/Janpot/escape-html-template-tag/blob/master/src/index.ts
8
9const ENTITIES: {
10 [key: string]: string
11} = {
12 '&': '&',
13 '<': '<',
14 '>': '>',
15 '"': '"',
16 "'": ''',
17 '/': '/',
18 '`': '`',
19 '=': '=',
20}
21
22const ENT_REGEX = new RegExp(Object.keys(ENTITIES).join('|'), 'g')
23
24function escapehtml(unsafe: Sub): string {
25 if (Array.isArray(unsafe)) {
26 return unsafe.map(escapehtml).join('')
27 }
28 if (unsafe instanceof HtmlSafeString) {
29 return unsafe.toString()
30 }
31 return String(unsafe).replace(ENT_REGEX, char => ENTITIES[char])
32}
33
34type Sub = HtmlSafeString | string | (HtmlSafeString | string)[]
35
36export class HtmlSafeString {
37 private _parts: readonly string[]
38 private _subs: readonly Sub[]
39 constructor(parts: readonly string[], subs: readonly Sub[]) {
40 this._parts = parts
41 this._subs = subs
42 }
43
44 toString(): string {
45 let result = this._parts[0]
46 for (let i = 1; i < this._parts.length; i++) {
47 result += escapehtml(this._subs[i - 1]) + this._parts[i]
48 }
49 return result
50 }
51}
52
53export function html(parts: TemplateStringsArray, ...subs: Sub[]) {
54 return new HtmlSafeString(parts, subs)
55}
56
57export const renderHandleString = (profile: AnyProfileView) =>
58 profile.displayName
59 ? `${profile.displayName} (@${profile.handle})`
60 : `@${profile.handle}`
61
62class HeadHandler {
63 profile: PResp
64 url: string
65 constructor(profile: PResp, url: string) {
66 this.profile = profile
67 this.url = url
68 }
69 async element(element) {
70 const view = this.profile.data
71
72 const description = view.description
73 ? html`
74 <meta name="description" content="${view.description}" />
75 <meta property="og:description" content="${view.description}" />
76 `
77 : ''
78 const img = view.banner
79 ? html`
80 <meta property="og:image" content="${view.banner}" />
81 <meta name="twitter:card" content="summary_large_image" />
82 `
83 : view.avatar
84 ? html`<meta name="twitter:card" content="summary" />`
85 : ''
86 element.append(
87 html`
88 <meta property="og:site_name" content="Witchsky" />
89 <meta property="og:type" content="profile" />
90 <meta property="profile:username" content="${view.handle}" />
91 <meta property="og:url" content="${this.url}" />
92 <meta property="og:title" content="${renderHandleString(view)}" />
93 ${description} ${img}
94 <meta name="twitter:label1" content="Account DID" />
95 <meta name="twitter:value1" content="${view.did}" />
96 <link
97 rel="alternate"
98 href="at://${view.did}/app.bsky.actor.profile/self" />
99 `,
100 {html: true},
101 )
102 }
103}
104
105class TitleHandler {
106 profile: PResp
107 constructor(profile: PResp) {
108 this.profile = profile
109 }
110 async element(element) {
111 element.setInnerContent(renderHandleString(this.profile.data))
112 }
113}
114
115class NoscriptHandler {
116 profile: PResp
117 constructor(profile: PResp) {
118 this.profile = profile
119 }
120 async element(element) {
121 const view = this.profile.data
122
123 element.append(
124 html`
125 <div id="bsky_profile_summary">
126 <h3>Profile</h3>
127 <p id="bsky_display_name">${view.displayName ?? ''}</p>
128 <p id="bsky_handle">${view.handle}</p>
129 <p id="bsky_did">${view.did}</p>
130 <p id="bsky_profile_description">${view.description ?? ''}</p>
131 </div>
132 `,
133 {html: true},
134 )
135 }
136}
137
138export async function onRequest(context) {
139 const agent = new AtpAgent({service: 'https://public.api.bsky.app/'})
140 const {request, env} = context
141 const origin = new URL(request.url).origin
142
143 const base = env.ASSETS.fetch(new URL('/', origin))
144 try {
145 const profile = await agent.getProfile({
146 actor: context.params.handleOrDID,
147 })
148 return new HTMLRewriter()
149 .on(`head`, new HeadHandler(profile, request.url))
150 .on(`title`, new TitleHandler(profile))
151 .on(`noscript`, new NoscriptHandler(profile))
152 .transform(await base)
153 } catch (e) {
154 console.error(e)
155 return await base
156 }
157}